package com.lei.sandbox;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.ArrayUtil;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.lei.sandbox.model.ExecuteCodeRequest;
import com.lei.sandbox.model.ExecuteCodeResponse;
import com.lei.sandbox.model.ExecuteMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import javax.annotation.PreDestroy;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;

/**
 * 远程代码沙箱（实际调用接口）
 *
 * @author lei
 */
@Component
@Slf4j
public class JavaDockerCodeSandbox extends CodeSandboxTemplate {

    private static final long TIME_OUT = 5000L;
    public static final Boolean FIRST_INIT = true;
    
    // 容器池，用于复用容器
    private static final ConcurrentHashMap<String, ContainerInfo> CONTAINER_POOL = new ConcurrentHashMap<>();
    // 最大容器池大小
    private static final int MAX_CONTAINER_POOL_SIZE = 10;
    // 容器最大空闲时间（毫秒）
    private static final long MAX_IDLE_TIME = 300000; // 5分钟
    
    // 用于存储Docker客户端和HttpClient，以便在关闭时正确释放资源
    private ApacheDockerHttpClient httpClient;
    private DockerClient dockerClient;

    @Value("${docker.url:example}")
    private String dockerUrl;

    // 添加显式的 executeCode 方法重写，与 JavaNativeCodeSandbox 保持一致
    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        try {
            return super.executeCode(executeCodeRequest);
        } catch (Exception e) {
            // 确保所有异常都被正确处理
            return getErrorResponse(e);
        }
    }

    /**
     * //3、创建容器，上传编译文件
     * @param userCodeFile
     * @param inputList
     * @return
     */
    @Override
    public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {

        String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();

        // 检查编译后的class文件是否存在
        File classFile = new File(userCodeParentPath, "Main.class");
        System.out.println("Main.class文件是否存在: " + classFile.exists());

        //todo 本地Unix Socket连接  链接本地docker 改变docker.url即可

        //todo 链接远程服务器docker
        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerUrl)
                .build();

        // 使用httpclient5实现
        httpClient = new ApacheDockerHttpClient.Builder()
                .dockerHost(config.getDockerHost())
                .connectionTimeout(Duration.ofSeconds(30))
                .responseTimeout(Duration.ofSeconds(45))
                .build();

        dockerClient = DockerClientBuilder.getInstance(config)
                .withDockerHttpClient(httpClient)
                .build();

        //1、拉取镜像
        String image = "openjdk:11-jre-slim";
        if (FIRST_INIT) {
            // 先检查本地是否存在镜像
            if (!isImageExists(dockerClient,image)) {
                // 只有本地不存在时才拉取
                PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
                PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
                    @Override
                    public void onNext(PullResponseItem item) {
                        System.out.println("下载镜像：" + item.getStatus());
                        super.onNext(item);
                    }
                };
                try {
                    pullImageCmd
                            .exec(pullImageResultCallback)
                            .awaitCompletion(30, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    System.out.println("镜像拉取异常");
                    throw new RuntimeException(e);
                }
                System.out.println("下载完成");
            } else {
                System.out.println("使用本地镜像：" + image);
            }
        }

        // 获取或创建容器
        String containerId = getOrCreateContainer(dockerClient, image);

        // 将编译后的class文件复制到容器
        copyClassFileToContainer(dockerClient, containerId, userCodeParentPath);

        // 执行代码
        List<ExecuteMessage> executeMessageList = executeCodeInContainer(dockerClient, containerId, inputList);

        // 检查是否需要将容器返回池中或销毁
        returnContainerToPoolIfNeeded(containerId);

        return executeMessageList;
    }

    /**
     * 获取或创建容器
     */
    private String getOrCreateContainer(DockerClient dockerClient, String image) {
        // 首先尝试从池中获取可用容器
        String containerId = getContainerFromPool();
        if (containerId != null) {
            System.out.println("使用池中的容器: " + containerId);
            return containerId;
        }

        // 没有可用容器，创建新容器
        System.out.println("创建新的容器");
        return createNewContainer(dockerClient, image);
    }

    /**
     * 从池中获取容器
     */
    private String getContainerFromPool() {
        // 清理过期容器
        cleanupExpiredContainers();

        // 查找可用容器
        for (String containerId : CONTAINER_POOL.keySet()) {
            ContainerInfo containerInfo = CONTAINER_POOL.get(containerId);
            if (containerInfo != null && containerInfo.isAvailable()) {
                synchronized (containerInfo) {
                    if (containerInfo.isAvailable()) {
                        containerInfo.setAvailable(false);
                        containerInfo.setLastUsedTime(System.currentTimeMillis());
                        return containerId;
                    }
                }
            }
        }
        return null;
    }

    /**
     * 创建新容器
     */
    private String createNewContainer(DockerClient dockerClient, String image) {
        CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
        String profileConfig = ResourceUtil.readUtf8Str("profile.json");
        HostConfig hostConfig = new HostConfig();
        hostConfig.withMemory(100 * 1000 * 1000L);
        hostConfig.withMemorySwap(0L);
        hostConfig.withCpuCount(1L);
        hostConfig.withSecurityOpts(List.of("seccomp=" + profileConfig));
        hostConfig.withAutoRemove(true); // 容器退出时自动删除

        // 根据 dockerUrl 判断是否远程
        boolean isRemoteDocker = dockerUrl != null && dockerUrl.startsWith("tcp://");
        if (isRemoteDocker) {
            System.out.println("运行模式: 远程 Docker（禁用 bind，采用拷贝文件方式）");
        } else {
            System.out.println("运行模式: 本地 Docker（使用 bind 挂载）");
        }

        // 为容器生成唯一名称
        String containerName = "oj-sandbox-jdk-" + UUID.randomUUID().toString();

        CreateContainerResponse createContainerResponse = containerCmd
                .withName(containerName)
                .withHostConfig(hostConfig)
                .withNetworkDisabled(true)  //关闭网络
                .withReadonlyRootfs(false)  // 始终设置为可写，确保能创建临时文件
                .withAttachStderr(true)
                .withAttachStdin(true)
                .withAttachStdout(true)
                .withTty(false)
//                .withCmd("sh", "-lc", "tail -f /dev/null")
                // 保持容器前台进程常驻，便于后续 exec，5分钟后自动退出
                .withCmd("timeout", "300", "tail", "-f", "/dev/null")
                .exec();

        String containerId = createContainerResponse.getId();

        // 启动容器
        dockerClient.startContainerCmd(containerId).exec();

        // 将容器添加到池中（如果池未满）
        if (CONTAINER_POOL.size() < MAX_CONTAINER_POOL_SIZE) {
            CONTAINER_POOL.put(containerId, new ContainerInfo(containerId, System.currentTimeMillis()));
        }

        return containerId;
    }

    /**
     * 将class文件复制到容器
     */
    private void copyClassFileToContainer(DockerClient dockerClient, String containerId, String userCodeParentPath) {
        try {
            File mainClassFile = new File(userCodeParentPath, "Main.class");
            if (!mainClassFile.exists()) {
                throw new RuntimeException("Main.class 不存在，无法复制到容器");
            }
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(bos)) {
                // 先创建目录条目，确保 /app 被创建
                TarArchiveEntry dirEntry = new TarArchiveEntry("app/");
                dirEntry.setMode(0755);
                dirEntry.setSize(0);
                tarOut.putArchiveEntry(dirEntry);
                tarOut.closeArchiveEntry();

                // 将文件放入 app/ 目录，随后复制到容器根路径，这样会自动创建 /app
                TarArchiveEntry entry = new TarArchiveEntry("app/Main.class");
                entry.setSize(mainClassFile.length());
                tarOut.putArchiveEntry(entry);
                try (FileInputStream fis = new FileInputStream(mainClassFile)) {
                    byte[] buffer = new byte[8192];
                    int len;
                    while ((len = fis.read(buffer)) != -1) {
                        tarOut.write(buffer, 0, len);
                    }
                }
                tarOut.closeArchiveEntry();
                tarOut.finish();
            }
            try (InputStream tarStream = new ByteArrayInputStream(bos.toByteArray())) {
                dockerClient.copyArchiveToContainerCmd(containerId)
                        .withRemotePath("/")
                        .withTarInputStream(tarStream)
                        .exec();
            }
        } catch (Exception e) {
            System.err.println("复制 Main.class 到容器失败: " + e.getMessage());
            throw new RuntimeException("复制 Main.class 到容器失败: " + e.getMessage(), e);
        }
    }

    /**
     * 在容器中执行代码
     */
    private List<ExecuteMessage> executeCodeInContainer(DockerClient dockerClient, String containerId, List<String> inputList) {
        List<ExecuteMessage> executeMessageList = new ArrayList<>();
        for (String inputArgs : inputList) {
            //定时器
            StopWatch stopWatch = new StopWatch();
            // 交互模式：不再通过 args 传参，改为从 stdin 提供输入
            String[] cmdArray = new String[]{"java", "-cp", "/app", "Main"};

            ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
                    .withCmd(cmdArray)
                    .withAttachStderr(true)
                    .withAttachStdin(true)
                    .withAttachStdout(true)
                    .exec();
            //log.info("创建执行命令 ： {}" , execCreateCmdResponse);
            ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
            ExecuteMessage executeMessage = new ExecuteMessage();
            final String[] message = {null};
            final String[] errorMessage = {null};
            final StringBuilder outputBuffer = new StringBuilder(); // 使用缓冲区收集输出

            long time = 0L;
            final boolean[] timeout = {true};
            String execId = execCreateCmdResponse.getId();
            ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
                @Override
                public void onComplete() {
                    timeout[0] = false;
                    // 始终以缓冲区内容为准（可能多行）
                    message[0] = outputBuffer.toString();
                    super.onComplete();
                }

                @Override
                public void onNext(Frame frame) {
                    StreamType streamType = frame.getStreamType();
                    String payload = new String(frame.getPayload()); // 保留原始格式

                    if (StreamType.STDERR.equals(streamType)) {
                        errorMessage[0] = payload;
                        System.out.println("输出错误结果：" + errorMessage[0]);
                    } else {
                        // 累积 stdout 内容（可能分帧）
                        outputBuffer.append(payload);
                        System.out.println("输出结果：" + payload);
                    }
                    super.onNext(frame);
                }
            };

            final long[] maxMemory = {0L};
            // 添加监控标志位，确保每轮都是true开始
            final boolean[] monitoring = {true};
            //获取占用内存
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);
            ResultCallback<Statistics> resultCallback = statsCmd.exec(new ResultCallback<Statistics>() {

                private long startTime = System.currentTimeMillis();
                private static final long PRINT_INTERVAL = 100; // 增加到100ms
                private Long lastMemoryUsage = null; // 记录上次内存使用量

                @Override
                public void onNext(Statistics statistics) {
                    // 检查是否应该继续监控
                    if (!monitoring[0]) {
                        return;
                    }

                    Long currentMemoryUsage = statistics.getMemoryStats().getUsage();
                    long currentTime = System.currentTimeMillis();

                    if (currentMemoryUsage != null) {
                        // 立即更新最大内存值
                        maxMemory[0] = Math.max(currentMemoryUsage, maxMemory[0]);
                        // 打印条件：首次记录、内存变化、或达到打印间隔
                        if (lastMemoryUsage == null ||
                                !lastMemoryUsage.equals(currentMemoryUsage) ||
                                (currentTime - startTime) >= PRINT_INTERVAL) {
                            lastMemoryUsage = currentMemoryUsage;
                            startTime = currentTime;
                        }
                    }
                }

                @Override
                public void onStart(Closeable closeable) {

                }

                @Override
                public void onError(Throwable throwable) {

                }

                @Override
                public void onComplete() {

                }

                @Override
                public void close() throws IOException {

                }
            });
            statsCmd.exec(resultCallback);

            try {
                Thread.sleep(200); // 给监控更多启动时间
                //开始定时
                stopWatch.start();
                // 通过 stdin 注入交互式输入（每个用例一行/或原样）
                byte[] stdinBytes = (inputArgs == null ? "" : (inputArgs + "\n")).getBytes(java.nio.charset.StandardCharsets.UTF_8);
                java.io.InputStream stdinStream = new java.io.ByteArrayInputStream(stdinBytes);
                dockerClient.execStartCmd(execId)
                        .withStdIn(stdinStream)
                        .exec(execStartResultCallback)
                        .awaitCompletion(TIME_OUT, TimeUnit.MILLISECONDS);
                //结束定时
                stopWatch.stop();
                time = stopWatch.getLastTaskTimeMillis();
            } catch (InterruptedException e) {
                System.out.println("程序执行异常");
                throw new RuntimeException(e);
            } finally {
                // 停止监控并关闭资源
                monitoring[0] = false; // 停止监控
                try {
                    if (statsCmd != null) {
                        statsCmd.close();
                    }
                } catch (Exception e) {
                    System.err.println("关闭statsCmd时出错: " + e.getMessage());
                }
            }
            String normalizedMessage = message[0];
            if (normalizedMessage != null) {
                normalizedMessage = normalizedMessage.replaceAll("[\\r\\n]+$", "");
            }
            executeMessage.setMessage(normalizedMessage);
            executeMessage.setErrorMessage(errorMessage[0]);
            executeMessage.setTime(time);
            // 如果内存监控失败，给一个合理的最小值（1MB）
            long finalMemory = maxMemory[0] > 0 ? maxMemory[0] : 1024 * 1024;
            executeMessage.setMemory(finalMemory);
            executeMessageList.add(executeMessage);
        }
        return executeMessageList;
    }

    /**
     * 根据池大小决定是否将容器返回池中
     */
    private void returnContainerToPoolIfNeeded(String containerId) {
        // 如果池未满，将容器标记为可用
        if (CONTAINER_POOL.size() < MAX_CONTAINER_POOL_SIZE) {
            ContainerInfo containerInfo = CONTAINER_POOL.get(containerId);
            if (containerInfo != null) {
                containerInfo.setAvailable(true);
                containerInfo.setLastUsedTime(System.currentTimeMillis());
            }
        } else {
            // 池已满，直接删除容器
            try {
                dockerClient.removeContainerCmd(containerId).withForce(true).exec();
                CONTAINER_POOL.remove(containerId);
            } catch (Exception e) {
                System.err.println("删除容器失败: " + e.getMessage());
            }
        }
    }

    /**
     * 清理过期容器
     */
    private void cleanupExpiredContainers() {
        long currentTime = System.currentTimeMillis();
        List<String> expiredContainers = new ArrayList<>();

        for (String containerId : CONTAINER_POOL.keySet()) {
            ContainerInfo containerInfo = CONTAINER_POOL.get(containerId);
            if (containerInfo != null && 
                (currentTime - containerInfo.getLastUsedTime()) > MAX_IDLE_TIME) {
                expiredContainers.add(containerId);
            }
        }

        // 删除过期容器
        for (String containerId : expiredContainers) {
            try {
                dockerClient.removeContainerCmd(containerId).withForce(true).exec();
                CONTAINER_POOL.remove(containerId);
                System.out.println("清理过期容器: " + containerId);
            } catch (Exception e) {
                System.err.println("清理过期容器失败: " + e.getMessage());
            }
        }
    }

    private String toDockerPath(String windowsPath) {
        String norm = windowsPath.replace("\\", "/");
        if (norm.length() >= 2 && norm.charAt(1) == ':') {
            char drive = Character.toLowerCase(norm.charAt(0));
            String rest = norm.substring(2);
            if (!rest.startsWith("/")) {
                rest = "/" + rest;
            }
            return "/" + drive + rest;
        }
        return norm.startsWith("/") ? norm : "/" + norm;
    }

    // 改进的镜像存在性检查方式
    private boolean isImageExists(DockerClient dockerClient, String imageName) {
        try {
            // 使用 inspectImageCmd 来检查镜像是否存在
            dockerClient.inspectImageCmd(imageName).exec();
            return true;
        } catch (Exception e) {
            // 如果抛出异常，说明镜像不存在
            return false;
        }
    }

    /**
     * 关闭Docker客户端资源，防止内存泄漏
     */
    @PreDestroy
    public void close() {
        // 关闭所有容器
        for (String containerId : CONTAINER_POOL.keySet()) {
            try {
                if (dockerClient != null) {
                    dockerClient.removeContainerCmd(containerId).withForce(true).exec();
                }
            } catch (Exception e) {
                System.err.println("关闭容器失败: " + e.getMessage());
            }
        }
        CONTAINER_POOL.clear();
        
        // 关闭Docker客户端和HTTP客户端
        if (dockerClient != null) {
            try {
                dockerClient.close();
            } catch (Exception e) {
                System.err.println("关闭Docker客户端失败: " + e.getMessage());
            }
        }
        
        if (httpClient != null) {
            try {
                httpClient.close();
            } catch (Exception e) {
                System.err.println("关闭HTTP客户端失败: " + e.getMessage());
            }
        }
    }
    
    /**
     * 容器信息类
     */
    private static class ContainerInfo {
        private String containerId;
        private boolean available;
        private long lastUsedTime;

        public ContainerInfo(String containerId, long lastUsedTime) {
            this.containerId = containerId;
            this.available = false; // 新创建的容器默认不可用，直到执行完成
            this.lastUsedTime = lastUsedTime;
        }

        public String getContainerId() {
            return containerId;
        }

        public boolean isAvailable() {
            return available;
        }

        public void setAvailable(boolean available) {
            this.available = available;
        }

        public long getLastUsedTime() {
            return lastUsedTime;
        }

        public void setLastUsedTime(long lastUsedTime) {
            this.lastUsedTime = lastUsedTime;
        }
    }
}